# -*- coding: utf-8 -*-

"""
Context managment.

Limitations:
    pop can be called from anywhere, also from within a think method of an
    active (topmost) context. After poping it the active context is not the
    topmost context anymore, but can still modify the context stack (since
    it is still in the think method).

.. todo::
    new API, maybe better or not???::

        # should top() be in __all__ ??
        __all__ = ["think", "draw", "init", "length"]

        def init(initial_context):
            _push(initial_context)

        def think(dt):
            action = _CONTEXT_STACK[-1].think(dt) # -> None  : do nothing
                                                  #    False : pop top context
                                                  #NewContext: push new context
            if action:
                _push(action)
            elif action is False:
                _pop()

        def draw(screen):
            if _CONTEXT_STACK:
                _CONTEXT_STACK[-1].draw(screen)

.. TODO::
    pass around a 'info' object (through push/pop)?? <- could make scene management easier
"""

import logging
logger = logging.getLogger("pyknic.context")


__all__ = ["Context", "push", "pop", "top", "length", "print_stack"]

# -----------------------------------------------------------------------------
# -----------------------------------------------------------------------------

# -----------------------------------------------------------------------------


# -----------------------------------------------------------------------------

_CONTEXT_STACK = []

def push(cont):
    """
    Pushes a new context on the stack. The enter method of the new context
    will be called when pushing it. If the stack is not empty, suspend will
    be called on the topmost context on the stack before pushing the new
    context on top of it.
    """
    cont.process_stack_push(_CONTEXT_STACK)
    if __debug__:
        logger.debug("CONTEXT: pushed " + str(cont))
        print_stack()


def pop(count = 1, do_resume=True):
    """
    Pops a context from the stack. The exit method of the removed context will
    be called. If a there is another context on the stack, the resume method of
    it will be called.

    :Parameters:
        count : int
            defaults to 1, number of contextes to pop
        do_resume : bool
            (default: True) This has only an effect if poping more than one context at once (count>1).
            If set to False then only the last context is resumed.
    """
    while count:
        count -= 1
        cont = None
        if _CONTEXT_STACK:
            cont = _CONTEXT_STACK.pop(-1)
            cont.process_stack_pop(_CONTEXT_STACK, (do_resume or count == 0))
            if __debug__:
                logger.debug("CONTEXT: poped " + str(cont))
                print_stack()

def top(idx=0):
    """
    The active context.

    :Parameters:
        idx : int
            default: 0 (topmost), the item at index position idx of the stack

    :returns:
        default: the topmost context of the stack or None
        otherwise the the item at index position idx or None if idx > length()
    """
    idx += 1
    if idx > len(_CONTEXT_STACK):
        return None
    return _CONTEXT_STACK[-1 * idx]

def length():
    """
    The number of items in the stack.

    :returns:
        number of items, integer >= 0
    """
    return len(_CONTEXT_STACK)

def print_stack():
    """
    Prints the current stack to the console. Should only be used for
    debugging purposes.
    """
    if __debug__:
        logger.debug("CONTEXT STACK:")
        for idx in range(0, length()):
            logger.debug("    %s %s" % (idx, top(idx)))
        logger.debug("")


# -----------------------------------------------------------------------------

class Context(object):
    """
    The context class.
    """
    def enter(self):
        """Called when this context is pushed onto the stack."""
        pass
    def exit(self):
        """Called when this context is popped off the stack."""
        pass
    def suspend(self):
        """Called when another context is pushed on top of this one."""
        pass
    def resume(self):
        """Called when another context is popped off the top of this one."""
        pass
    def think(self, delta_time):
        """Called once per frame"""
        pass
    def draw(self, screen):
        """Refresh the screen"""
        pass

    def process_stack_push(self, context_stack):
        """
        Processes the stack push for instances of this class.
        """
        if context_stack:
            context_stack[-1].suspend()
            if __debug__:
                logger.debug("CONTEXT: suspended " + str(context_stack[-1].__class__.__name__))
        context_stack.append(self)
        if __debug__:
            logger.debug("CONTEXT: pushed " + str(self.__class__.__name__))
        self.enter()
        if __debug__:
            logger.debug("CONTEXT: entered " + str(self.__class__.__name__))

    def process_stack_pop(self, context_stack, do_resume):
        """
        Processes the stack push for instances of this class.
        """
        self.exit()
        if __debug__:
            logger.debug("CONTEXT: exited " + str(self.__class__.__name__))
            logger.debug("CONTEXT: resume=" + str(do_resume))
        if context_stack and do_resume:
            # log first, because resume might pop a context from stack
            if __debug__:
                logger.debug("CONTEXT: resumed " + str(context_stack[-1].__class__.__name__))
            context_stack[-1].resume()

    push = staticmethod(push)
    pop = staticmethod(pop)
    top = staticmethod(top)
# -----------------------------------------------------------------------------

